﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MethodWorx.CMS.TemplateParser
{
    public class XmlParser
    {
        public XmlParser(string xml)
        {
            this.BuildTemplateOutline(xml);
        }

        public TemplateDocument Document { get; private set; }

        private enum CurrentParserContext
        {
            Text,
            Element,
            EndElement,
            AttributeName,
            AttributeValue
        }

        private void BuildTemplateOutline(string xml)
        {
            //
            //  ok, lets start at the root
            var context = CurrentParserContext.Text;
            bool inComment = false;
            this.Document = new TemplateDocument();
            var parent = (TemplateNode) this.Document;
            TemplateNode current = null;
            var elements = new Stack<TemplateElement>();
            bool inQuotes = false;
            var endElement = "";

            if (xml != null)
            {
                var templateString = xml + "  ";

                for (int i = 0; i < xml.Length; i++)
                {
                    var c = templateString[i];
                    var cn = templateString[i + 1];
                    var cnn = templateString[i + 2];
                    //  sort out the template node
                    current = CreateTemplateNode(xml, i, context, current, parent);

                    if (current is TemplateElement && !elements.Contains(current))
                    {
                        //  put the current element on the stack
                        elements.Push(current as TemplateElement);
                    }


                    switch (context)
                    {
                        case CurrentParserContext.EndElement:
                            if (c == '>')
                            {
                                endElement = endElement.Trim();
                                var peek = elements.Peek();
                                if (peek.Name == endElement)
                                {
                                    //  set the element end position
                                    if (!peek.ElementEndValueStringPosition.HasValue)
                                        peek.ElementEndValueStringPosition = i - endElement.Length;

                                    context = CurrentParserContext.Text;
                                    //  ok, this one matches
                                    elements.Pop();
                                    //  and set back to the parent
                                    parent = peek.Parent;
                                    //  back to a null context
                                    peek.EndOffset = i + 1;
                                    current = null;
                                }
                                else
                                {
                                    current.Error = "End Tag Does Not Match Starting Tag - Line: " + GetLinePos(xml, peek.ElementNameStringPosition.Value);
                                }
                            }
                            else
                            {
                                var peek = elements.Peek();
                                peek.HasClosingTag = true;
                                endElement += c;
                            }
                            break;
                        case CurrentParserContext.Text:
                            //
                            //  the only thing to break us out of text into an element is '<'
                            if (!inComment && c == '<' && cn != '!')
                            {
                                if (cn != '/')
                                {
                                    //  ok, this is the end of the element
                                    context = CurrentParserContext.Element;
                                }
                                else
                                {
                                    context = CurrentParserContext.EndElement;
                                    i++;
                                    endElement = "";
                                }

                            }
                            else
                            {
                                var tt = current as TemplateText;
                                if (!tt.TextStartingPosition.HasValue)
                                    tt.TextStartingPosition = i;
                                tt.Text += c;
                                if (cn == '!')
                                {
                                    tt.IsComment = true;
                                    inComment = true;
                                }

                                //  we are on longer in a comment
                                if (inComment && c == '-' && cn == '-' && cnn == '>')
                                    inComment = false;
                            }
                            break;
                        case CurrentParserContext.Element:
                            //  ok, we want an element name
                            if (Char.IsWhiteSpace(c)) //  this is when we look for elements
                            {
                                context = CurrentParserContext.AttributeName;
                                parent = current; //  we are parented on the current
                            }
                            else if (c == '>') //  end of an element
                            {
                                //  we are parented here
                                context = CurrentParserContext.Text;
                                parent = current;
                                current = null;
                            }
                            else if (c == '/')
                            {
                                if (cn == '>')
                                {
                                    context = CurrentParserContext.EndElement;
                                    endElement = elements.Peek().Name;
                                    ((TemplateElement) (current)).ElementEndValueStringPosition = i;
                                }
                            }
                            else
                            {
                                var te = current as TemplateElement;
                                if (!te.ElementNameStringPosition.HasValue)
                                    te.ElementNameStringPosition = i;

                                te.Name += c; //  append this to the name
                            }
                    break;
                        case CurrentParserContext.AttributeName:
                            //  ok, we want an attribute name
                            if (c == '=')
                            {
                                context = CurrentParserContext.AttributeValue;
                            }
                            else if(c == '>')
                            {
                                //  this is the end of an element
                                context = CurrentParserContext.Element;
                                current = current.Parent;   //  go back to the element
                                i--;    //  and go back a character
                            }
                            else if (c == '/')
                            {
                                if (cn == '>')
                                {
                                    current.EndOffset = i;
                                    context = CurrentParserContext.EndElement;
                                    endElement = elements.Peek().Name;
                                    current = current.Parent;   //  go back to the element
                                }
                            }
                            else
                            {
                                var tan = current as TemplateAttribute;
                                if (!tan.AttributeNameStringPosition.HasValue)
                                    tan.AttributeNameStringPosition = i;
                                tan.Name = (tan.Name ?? "").Trim() + c;
                            }
                            break;
                        case CurrentParserContext.AttributeValue:
                            if (string.IsNullOrEmpty(((TemplateAttribute)current).Value) && !inQuotes && c == '"')
                                inQuotes = true;
                            else if ((inQuotes && c == '"') || (!inQuotes && Char.IsWhiteSpace(c) && c != '>'))   //  end of quotes or space
                            {
                                inQuotes = false;
                                //  this is the end, so go back to the element
                                context = CurrentParserContext.AttributeName;
                                current.EndOffset = i;
                                current = null;
                            }
                            else if((!inQuotes && c == '>'))
                            {
                                //  this is the end of an element
                                context = CurrentParserContext.Element;
                                current = current.Parent;   //  go back to the element
                                i--;    //  and go back a character
                            }
                            else
                            {
                                var tav = current as TemplateAttribute;
                                if (!tav.AttributeValueStringPosition.HasValue)
                                    tav.AttributeValueStringPosition = i;
                                //  set the value here
                                tav.Value += c;
                                
                            }
                            break;
                    }
                }
            }
        }

        private int GetLinePos(string xml, int pos)
        {
            int linePos = 0;
            for(int c = 0; c < pos && c < xml.Length; c++)
            {
                if (xml[c] == '\n')
                    linePos++;
            }

            return linePos;

        }

        private TemplateNode CreateTemplateNode(string xml, int i, CurrentParserContext context, TemplateNode current, TemplateNode parent)
        {
            switch (context)
            {
                case CurrentParserContext.Text:
                    if (!(current is TemplateText))
                    {
                        current = new TemplateText();
                        current.Offset = i;
                        parent.AddNode(current);
                    }
                    break;
                case CurrentParserContext.AttributeName:
                    if (!(current is TemplateAttribute))
                    {
                        current = new TemplateAttribute();
                        current.Offset = i;
                        parent.AddNode(current);
                    }
                    break;
                case CurrentParserContext.Element:
                    if (!(current is TemplateElement))
                    {
                        current = new TemplateElement();
                        current.Offset = i - 1;
                        parent.AddNode(current);
                    }
                    break;
            }
            current.ParsedBuffer = xml;
            return current;
        }
    }
}
